'Estado' es un término de programación común que experimentan todos los desarrolladores a medida que avanzan desde la programación inicial hasta la programación de nivel intermedio. Entonces, ¿qué significa exactamente el término "Estado"?
En general, el estado de un objeto es simplemente la instantánea actual del objeto o una parte de él. Mientras tanto, en informática, el estado de un programa se define como su posición con respecto a las entradas previamente almacenadas. En este contexto, el término "estado" se usa de la misma manera que en la ciencia: el estado de un objeto, como un gas, líquido o sólido, representa su naturaleza física actual, y el estado de un programa de computadora refleja sus valores o contenidos actuales.
Las entradas almacenadas se conservan como variables o constantes en un programa de computadora. Al evaluar el estado de un programa, los desarrolladores pueden examinar los valores contenidos en estas entradas. El estado del programa puede cambiar mientras se ejecuta: las variables pueden cambiar y los valores de la memoria pueden cambiar. Una variable de control, como la que se usa en un ciclo, por ejemplo, cambia el estado del programa en cada iteración. El examen del estado actual de un programa se puede utilizar para probar o analizar el código base.
En sistemas más simples, la administración de estado se maneja con frecuencia con sentencias if-else, if-then-else, try-catch o banderas booleanas; sin embargo, esto es inútil cuando hay demasiados estados imaginables en un programa. Pueden conducir a un código complicado y torpe que es difícil de entender, mantener y depurar.
Una desventaja de las cláusulas if-else o booleanos es que pueden volverse bastante extensos, y agregar otro estado es difícil porque requiere reescribir el código de muchas clases diferentes. Suponga que desea crear un juego que tenga el menú principal, un bucle de juego y una pantalla de finalización.
Construyamos un reproductor de video, por ejemplo:
class Video: def __init__(self, source): self.source = source self.is_playing = False self.is_paused = False self.is_stopped = True # A video can only be played when paused or stopped def play(self): if not self.is_playing or self.is_paused: # Make the call to play the video self.is_playing = True self.is_paused = False else: raise Exception( 'Cannot play a video that is already playing.' ) # A video can only be paused when it is playing def pause(self): if self.is_playing: # Make the call to pause the video self.is_playing = False self.is_paused = True else: raise Exception( 'Cannot pause a video that is not playing' ) # A video can only be stopped when it is playing or paused def stop(self): if self.is_playing or self.is_paused: # Make the call to stop the video self.is_playing = False self.is_paused = False else: raise Exception( 'Cannot stop a video that is not playing or paused' )
El fragmento de código anterior es una implementación if-else de una aplicación de reproductor de video simple, donde los tres estados básicos son: reproducción, pausa y detención. Sin embargo, si tratamos de agregar más estados, el código rápidamente se volverá complejo, inflado, repetitivo y difícil de entender y probar. Veamos cómo se ve el código al agregar otro estado 'rebobinar':
class Video: def __init__(self, source): self.source = source self.is_playing = False self.is_paused = False self.is_rewinding = False self.is_stopped = True # A video can only be played when it is paused or stopped or rewinding def play(self): if self.is_paused or self.is_stopped or self.is_rewinding: # Make the call to play the video self.is_playing = True self.is_paused = False self.is_stopped = False self.is_rewinding = False else: raise Exception( 'Cannot play a video that is already playing.' ) # A video can only be paused when it is playing or rewinding def pause(self): if self.is_playing or self.is_rewinding: # Make the call to pause the video self.is_playing = False self.is_paused = True self.is_rewinding = False self.is_stopped = False else: raise Exception( 'Cannot pause a video that is not playing or rewinding' ) # A video can only be stopped when it is playing or paused or rewinding def stop(self): if self.is_playing or self.is_paused or self.is_rewinding: # Make the call to stop the video self.is_playing = False self.is_paused = False self.is_stopped = True self.is_rewinding = False else: raise Exception( 'Cannot stop a video that is not playing or paused or rewinding' ) # 4. A video can only be rewinded when it is playing or paused. def rewind(self): if self.is_playing or self.is_paused: # Make the call to rewind the video self.is_playing = False self.is_paused = False self.is_stopped = False self.is_rewinding = True else: raise Exception( 'Cannot rewind a video that is not playing or paused' )
Sin el patrón de estado, tendría que examinar el estado actual del programa en todo el código, incluidos los métodos de actualización y dibujo. Si desea agregar un cuarto estado, como una pantalla de configuración, deberá actualizar el código de muchas clases distintas, lo cual es un inconveniente. Aquí es donde la idea de las máquinas de estado resulta útil.
Las máquinas de estado no son un concepto novedoso en informática; son uno de los patrones de diseño básicos utilizados en el negocio del software. Está más orientado al sistema que a la codificación y se utiliza para modelar casos de uso.
Veamos un ejemplo simple de la vida real de contratar un taxi a través de Uber:
La pantalla 1 es la primera pantalla que ven todos los usuarios en este caso de uso y es independiente. La Pantalla 2 depende de la Pantalla 1, y no podrá ir a la Pantalla 2 hasta que proporcione datos precisos en la Pantalla 1. Del mismo modo, la Pantalla 3 depende de la Pantalla 2, mientras que la Pantalla 4 depende de la Pantalla 3. Si ninguno de los dos ni su conductor cancela su viaje, pasará a la pantalla 4, donde no podrá planificar otro viaje hasta que finalice el actual.
Digamos que está lloviendo severamente y ningún conductor acepta su viaje o no se encuentra ningún conductor disponible en su región para terminar su viaje; aparece una notificación de error advirtiéndole de la falta de disponibilidad del controlador, y permanece en la pantalla 3. Todavía puede volver a la pantalla 2, a la pantalla 1 e incluso a la primera pantalla.
Está en un paso diferente del proceso de reserva de taxis y solo puede pasar al siguiente nivel si una acción específica en la etapa actual tiene éxito. Por ejemplo, si ingresa una ubicación incorrecta en la pantalla 1, no podrá continuar con la pantalla 2 y no podrá continuar con la pantalla 3 a menos que elija una opción de viaje en la pantalla 2, pero es posible que vuelve siempre a la etapa anterior a menos que tu viaje ya esté reservado.
En el ejemplo anterior, hemos dividido el proceso de reserva de un taxi en varias actividades, cada una de las cuales puede o no estar autorizada para llamar a otra actividad según el estado de la reserva. Se utiliza una máquina de estado para modelar esto. En principio, cada una de estas etapas/estados debe ser autónomo, convocando uno al siguiente solo después de que el actual haya terminado, con éxito o no.
En palabras más técnicas, la máquina de estados nos permite dividir una gran acción complicada en una sucesión de actividades separadas más pequeñas, como la actividad de reserva de taxis en el ejemplo anterior.
Los eventos conectan tareas más pequeñas y el cambio de un estado a otro se conoce como transición. Normalmente llevamos a cabo algunas acciones después de cambiar de un estado a otro, como crear una reserva en el back-end, emitir una factura, guardar los datos analíticos del usuario, capturar los datos de la reserva en una base de datos, activar el pago una vez finalizado el viaje, etc. .
Por lo tanto, la fórmula general para una máquina de estados se puede dar como:
Estado actual + Alguna acción / Evento = Otro estado
Veamos cómo se vería una máquina de estado diseñada para una aplicación de reproductor de video simple:
Y podemos implementarlo en el código usando transiciones de la siguiente manera:
from transitions import Machine class Video: # Define the states PLAYING = 'playing' PAUSED = 'paused' STOPPED = 'stopped' def __init__(self, source): self.source = source # Define the transitions transitions = [ # 1. A video can only be played when it is paused or stopped. {'trigger': 'play', 'source': self.PAUSED, 'dest': self.PLAYING}, {'trigger': 'play', 'source': self.STOPPED, 'dest': self.PLAYING}, # 2. A video can only be paused when it is playing. {'trigger': 'pause', 'source': self.PLAYING, 'dest': self.PAUSED}, # 3. A video can only be stopped when it is playing or paused. {'trigger': 'stop', 'source': self.PLAYING, 'dest': self.STOPPED}, {'trigger': 'stop', 'source': self.PAUSED, 'dest': self.STOPPED}, ] # Create the state machine self.machine = Machine{ model = self, transitions = transitions, initial = self.STOPPED } def play(self): pass def pause(self): pass def stop(self): pass
Ahora, en caso de que queramos agregar otro estado, digamos rebobinar, podemos hacerlo fácilmente de la siguiente manera:
from transitions import Machine class Video: # Define the states PLAYING = 'playing' PAUSED = 'paused' STOPPED = 'stopped' REWINDING = 'rewinding' # new def __init__(self, source): self.source = source # Define the transitions transitions = [ # 1. A video can only be played when it is paused or stopped. {'trigger': 'play', 'source': self.PAUSED, 'dest': self.PLAYING}, {'trigger': 'play', 'source': self.STOPPED, 'dest': self.PLAYING}, {'trigger': 'play', 'source': self.REWINDING, 'dest': self.PLAYING}, # new # 2. A video can only be paused when it is playing. {'trigger': 'pause', 'source': self.PLAYING, 'dest': self.PAUSED}, {'trigger': 'pause', 'source': self.REWINDING, 'dest': self.PAUSED}, # new # 3. A video can only be stopped when it is playing or paused. {'trigger': 'stop', 'source': self.PLAYING, 'dest': self.STOPPED}, {'trigger': 'stop', 'source': self.PAUSED, 'dest': self.STOPPED}, {'trigger': 'stop', 'source': self.REWINDING, 'dest': self.STOPPED}, # new # 4. A video can only be rewinded when it is playing or paused. {'trigger': 'rewind', 'source': self.PLAYING, 'dest': self.REWINDING}, #new {'trigger': 'rewind', 'source': self.PAUSED, 'dest': self.REWINDING}, # new ] # Create the state machine self.machine = Machine{ model = self, transitions = transitions, initial = self.STOPPED } def play(self): pass def pause(self): pass def stop(self): pass def rewind(self): pass
Por lo tanto, podemos ver cómo las máquinas de estado pueden simplificar una implementación compleja y evitar que escribamos código incorrecto. Habiendo aprendido las capacidades de las máquinas de estado, ahora es importante comprender por qué y cuándo usar las máquinas de estado.
Las máquinas de estado se pueden utilizar en aplicaciones que tienen distintos estados. Cada etapa puede conducir a uno o más estados posteriores, así como finalizar el flujo del proceso. Una máquina de estado emplea la entrada del usuario o cálculos en el estado para elegir a qué estado ingresar a continuación.
Muchas aplicaciones requieren una etapa de "inicialización", seguida de un estado predeterminado que permite una amplia gama de acciones. Las entradas anteriores y presentes, así como los estados, pueden tener un impacto en las acciones que se ejecutan. Las medidas de limpieza se pueden llevar a cabo cuando el sistema está "apagado".
Una máquina de estado puede ayudarnos a conceptualizar y administrar esas unidades de manera más abstracta si podemos dividir un trabajo enormemente complejo en unidades independientes más pequeñas, donde simplemente necesitamos describir cuándo un estado puede hacer la transición a otro estado y qué sucede cuando ocurre la transición. No necesitamos preocuparnos por cómo ocurre la transición después de la instalación. Después de eso, solo tenemos que pensar en cuándo y qué, no en cómo.
Además, las máquinas de estado nos permiten ver todo el proceso de estado de una manera muy predecible; una vez que se establecen las transiciones, no tenemos que preocuparnos por la mala gestión o las transiciones de estado erróneas; la transición incorrecta puede ocurrir solo si la máquina de estado está configurada correctamente. Tenemos una vista integral de todos los estados y transiciones en una máquina de estado.
Si no usamos una máquina de estados, no podemos visualizar nuestros sistemas en varios estados posibles, o estamos acoplando nuestros componentes a sabiendas o no, o estamos escribiendo muchas condiciones if-else para simular transiciones de estado, que complica las pruebas unitarias y de integración porque debemos asegurarnos de que todos los casos de prueba estén escritos para validar la posibilidad de todas las condiciones y ramificaciones utilizadas.
Las máquinas de estado, además de su capacidad para desarrollar algoritmos de toma de decisiones, son formas funcionales de planificación de aplicaciones. A medida que las aplicaciones se vuelven más complejas, crece la necesidad de un diseño eficaz.
Los diagramas de estado y los diagramas de flujo son útiles y, en ocasiones, esenciales durante todo el proceso de diseño. Las máquinas de estado son importantes no solo para la planificación de aplicaciones, sino que también son fáciles de crear.
Las siguientes son algunas de las principales ventajas de las máquinas de estado en la informática moderna:
No todo lo relacionado con las máquinas de estado es bueno, a veces también pueden generar inconvenientes y desafíos. Estos son algunos de los problemas comunes con las máquinas de estado:
Al usar una máquina de estado, su sistema idealmente debería tener dos componentes lógicos:
La máquina de estado puede considerarse como la infraestructura que impulsa las transiciones de estado; verifica las transiciones de estado y ejecuta acciones configuradas antes, durante y después de una transición; sin embargo, no debe saber qué lógica de negocio se hace en esas acciones.
Por lo tanto, en general, es una buena idea aislar la máquina de estado de la lógica comercial principal mediante el uso de abstracciones correctas; de lo contrario, administrar el código sería una pesadilla.
Aquí hay algunos otros escenarios de la vida real en los que debemos emplear la lógica de la máquina de estado con precaución:
Las siguientes son algunas de las aplicaciones prácticas que se benefician del concepto de máquinas de estado en nuestra vida diaria:
Cuando compra algo en un sitio de comercio electrónico en línea, por ejemplo, pasa por varias fases, como pedido, empaquetado, enviado, cancelado, entregado, pagado, reembolsado, etc. La transición ocurre automáticamente a medida que las cosas se mueven a través de un almacén o centro logístico y se escanean en varias etapas, como cuando un usuario cancela o desea un reembolso.
La noción de máquinas de estado es extremadamente útil en programación. No solo agiliza el proceso de desarrollo de aplicaciones de casos de uso más complicados, sino que también reduce el trabajo de desarrollo necesario. Proporciona una comprensión más simple y elegante de los acontecimientos modernos y, cuando se aplica correctamente, puede hacer milagros.